#!/usr/local/bin/python2.6

import os,sys,pwd
from optparse import OptionParser, OptionGroup

VERSION = "0.3.0.0"

license_header = "ISC License\n\nCopyright (c) 2009, Ryan Boggs <" + "moc.liamg@sggobmr"[::-1] + ">"

license = sys.argv[0] + "\n" + license_header + """

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

"""

history = """
In case anyone is interested, this is sort of the changelog for this
script as written by the original author...

0.3.0.0          Switched to python's OptionParser. Updated shebang to
                 use actual python executable instead of relying on env.
                 Added the more program to the copy list.  Updated the
                 print commands to use the new print() function.
                 
0.2.3.0          Modified the script to copy over the Python
                 executables to the chroot and create the
                 symbolic links for them inside chroot

0.2.2.0          Actually make this script build the devices in
                 chroot instead of copying them.  Also, add
                 the various random devices

0.2.1.0          Ofuscated author's email address for public
                 posting

0.2.0.0          Added the commands to copy /dev/null to chroot
                 since it is needed by Django. Also added this 
                 history to the file
         
0.1.3.0          Added the -MYSQL argument to the program to 
                 copy all of the mysqldb files and dependencies
                 to chroot.  Still experimental since I don't
                 use MySQL.  Also added -PGSQL as well to 
                 distinguish which database connector the user
                 wants to use (ie, Postgresql, MySQL, or both)

0.1.2.0          Added -c argument to let the user identify where
                 he or she has setup a chroot.  Other people may
                 setup chroots in other areas besides /var/www

0.1.1.0          Added initial arguments to display the license
                 and help texts to the user if he or she wants
                 to see them
         
"""

help_description = """
This script is designed to help the user setup python and django
in a Chrooted Apache environment on an OpenBSD system.  Currently,
it only copies the program and library files needed for Python,
Django, mysqldb, and psycopg2.  This script is not designed to
install any of the necessary files onto the OpenBSD system.
That falls under the responsibility of the system administrator
through the use of OpenBSD's pkg_add utility.  For example:
pkg_add -i python py-psycopg2 py-mysql py-flup fcgi mod_fastcgi
Failure to do so may cause unexpected results within this script.
"""

def command_line_parser():
    # Sets the program version for display
    prog_ver = "%%prog %s" % VERSION
    
    # Create a parser for command line arguments
    parser = OptionParser(version=prog_ver, description=help_description)

    parser.add_option("-i", "--hist", dest="history", action="store_true", default=False,
                      help="Displays the scripts changelog")
    parser.add_option("-l", "--lic", dest="license", action="store_true", default=False,
                      help="Displays the license for this script")
    parser.add_option("-c", dest="chroot_dir", action="store", type="string", default="/var/www",
                      help="Sets the chroot directory [default: %default]")
    
    db_group = OptionGroup(parser, "Database Options", 
                                   "Options to move necessary database libraries to chrooted area")
    
    db_group.add_option("--mysql", dest="mysql", action="store_true", default=False,
                        help="Moves all of the files needed for the python MySQLdb library")
    
    db_group.add_option("--pgsql", dest="pgsql", action="store_true", default=False,
                        help="Moves all of the files needed for the python psycopg2 library")

    parser.add_option_group(db_group)
    
    return parser

if __name__ == "__main__":
    # Get all command line arguments
    parser = command_line_parser()
    opts = parser.parse_args()[0]
    
    # python_version identifies which version of python that is being used to 
    # run the script so it knows what version of python to copy
    python_version = "%s.%s" % (str(sys.version_info[0]), str(sys.version_info[1]))
    
    # If -l or --lic is passed to the script, the script license is printed
    # on the screen and the script exits
    if opts.license:
        print license
    
    # If -i or --hist is passed to the script, the script prints the changelog
    # for the script on the screen and exits
    elif opts.history:
        print history
        
    # Make sure that that user selects at least one type of db connection.
    # Otherwise, the error will be presented to the user
    elif not opts.mysql and not opts.pgsql:
        print("Please select at least one database connection to move into Apache Chroot")
        
    # OpenBSD is the only secure operating system I know that chroots apache by default.  
    # Since the file paths are specific to OpenBSD, that's the only system that is allowed
    # to run this script.
    elif os.uname()[0] != 'OpenBSD':
        print("This script was written and tested specifically for OpenBSD systems only")
        
    # Since this script needs to be able to copy files and make directories in the 
    # chroot area (usually /var/www), root access is needed. Thus, the script will 
    # error out if this script is not running as "root"
    elif pwd.getpwuid(os.getuid())[0] != 'root':
        print("Root access is required to move files and create directories in the Apache Chroot ")
        print("directory.  The preferred method of accomplishing this is through the \"sudo\" command.")
        print("Example: \"sudo python %s --pgsql\"" % sys.argv[0])
    
    # Otherwise, setup for copying
    else:
        # lstDirectories contains the names of the directories that may need to be created in the chroot directory
        lstDirectories = ["bin", "sbin", os.path.join("usr","local","bin"), os.path.join("usr","local","lib"), 
                          os.path.join("var","run"), os.path.join("usr","bin"), os.path.join("usr","lib"), 
                          os.path.join("usr","libexec"), "dev", "tmp"]
        
        # lstMySQLDirectories contains the additional directory needed for mysqldb
        lstMySQLDirectories = ["/usr/local/lib/mysql",]
        
        # lstMySqlLocalLibsFiles will contain the additional files that MySQL need
        lstMySqlLocalLibsFiles = []

        # lstLibs contains the beginning file names for all of the libraries needed for python to run in chroot from the /usr/lib directory
        # Please note: a few of these .so files may not be needed.  Better to be safe than sorry.
        lstLibs = ["libc", "libcrypto", "libm", "libcurses", "libncurses", "libreadline", "libssl", "libz", "libutil", "libstdc++", "libpthread", "libcom_err"]
        
        # Grabs all of the file names from the /usr/lib directory and stores them in the lstLibsFiles list
        lstLibsFiles = os.listdir("/usr/lib")
        
        # lstLocalLibs contains the beginning file names for all of the libraries needed for python to run in chroot from the /usr/local/lib directory
        lstLocalLibs = ["libpython" + python_version, "libfcgi", "libfcgi++"]
        
        # lstPgSqlLocalLibs contains the beginning file names for all of the libraries needed for psycopg2
        lstPgSqlLocalLibs = ["libxml", "libpq", "libecpg", "libecpg_compat", "libpgtypes"]
        
        # lstMySqlLocalLibs contains the beginning file names for all of the libraries needed for mysqldb
        lstMySqlLocalLibs = ["libmysqlclient", "libmysqlclient_r"]
        
        # Grabs all of the file names from the /usr/local/lib directory and stores them in the lstLocalLibsFiles list
        lstLocalLibsFiles = os.listdir("/usr/local/lib")
        
        # A dictionary of Python executable files that will need to be copied to and symbolically linked in the chroot area 
        dctPythonExecs = {"python" + python_version: "python",
                            "pydoc" + python_version: "pydoc",
                            "python" + python_version + "-config": "python-config"}
        
        # Depending on the db connector specified, the db connector specific .so files are appended to the 
        # appropriate list variables
        if opts.mysql:
            
            # Grabs all of the file names from the /usr/local/lib/mysql directory.  Needed for mysqldb.
            for mysqldir in lstMySQLDirectories:
                lstMySqlLocalLibsFiles += os.listdir(mysqldir)
                lstDirectories.append(mysqldir)
            
            lstLocalLibs += lstMySqlLocalLibs
            
        if opts.pgsql:
            lstLocalLibs += lstPgSqlLocalLibs
        
        # .......................................................................................................................
        # Begin chroot file copy process
        # .......................................................................................................................
        
        # Resets the umask so the directories are created with the correct permission.
        old_umask = os.umask(0)
        
        # Loops through the directory entries in lstDirectories and creates them if they do not exist in the chroot directory
        for dirname in lstDirectories:
            tmp_dir_path = os.path.join(opts.chroot_dir, dirname)
            if not os.path.exists(tmp_dir_path):
                print("Creating %s directory" % tmp_dir_path)
                os.makedirs(tmp_dir_path, 0755)
        
        # Copy the one off files to the chroot directory
        
        target_bin_path = os.path.join(opts.chroot_dir,"bin")
        target_sbin_path = os.path.join(opts.chroot_dir,"sbin")
        
        print("Copying /bin/sh to %s" % target_bin_path)
        cp_result = os.system("cp -p /bin/sh %s/" % target_bin_path)
        
        print("Copying /sbin/ldconfig to %s" % target_sbin_path)
        cp_result = os.system("cp -p /sbin/ldconfig " + target_sbin_path)
        
        # Loop through the dctPythonExecs dictionary to not only copy the python executables but create the 
        # symbolic links for them as well.
        for pyexec in dctPythonExecs.keys():
            print("Copying /usr/local/bin/%s to %s/usr/local/bin/%s" % (pyexec, opts.chroot_dir, pyexec))
            cp_result = os.system("cp -p /usr/local/bin/%s %s/usr/local/bin/%s" % (pyexec, opts.chroot_dir, pyexec))
            
            print("Creating Symbolic link for %s" % pyexec)
            cp_result = os.system("ln -sf /usr/local/bin/%s %s/usr/local/bin/%s" % (pyexec, opts.chroot_dir, dctPythonExecs[pyexec]))
        
        print("Copying /usr/local/bin/django-admin.py to %s/usr/local/bin" % opts.chroot_dir) 
        cp_result = os.system("cp -p /usr/local/bin/django-admin.py " + opts.chroot_dir + "/usr/local/bin")
        
        print("Copying /usr/bin/env to %s/usr/bin" % opts.chroot_dir)
        cp_result = os.system("cp -p /usr/bin/env " + opts.chroot_dir + "/usr/bin")
        
        print("Copying /usr/bin/more to %s/usr/bin" % opts.chroot_dir)
        cp_result = os.system("cp -p /usr/bin/more " + opts.chroot_dir + "/usr/bin")
        
        print("Copying /usr/libexec/ld.so to %s/usr/libexec" % opts.chroot_dir)
        cp_result = os.system("cp -p /usr/libexec/ld.so " + opts.chroot_dir + "/usr/libexec")
        
        # Build the devices in chroot that are usually needed for Django
        print("Building %s/dev/null device" % opts.chroot_dir)
        cp_result = os.system("mknod -m 666 " + opts.chroot_dir + "/dev/null c 2 2")

        print("Building %s/dev/random device" % opts.chroot_dir)
        cp_result = os.system("mknod -m 644 " + opts.chroot_dir + "/dev/random c 45 0")

        print("Building %s/dev/urandom device" % opts.chroot_dir)
        cp_result = os.system("mknod -m 644 " + opts.chroot_dir + "/dev/urandom c 45 2")

        # .......................................................................................................................
        # Both of these loops go through the files stored in lstLocalLibsFiles and lstLibsFiles and compares them to
        # the files that are in lstLocalLibs and lstLibs respectively and copies them to the chroot directory.  The idea here
        # is to not have to worry about library versions with every OpenBSD release while copying
        # .......................................................................................................................
        
        for libs in lstLibs:
            for lFile in lstLibsFiles:
                if libs + ".so" == lFile[:len(libs) + 3]:
                    print("Copying /usr/lib/" + lFile + " to " + opts.chroot_dir + "/usr/lib/" + lFile)
                    cp_result = os.system("cp -p /usr/lib/" + lFile + " " + opts.chroot_dir + "/usr/lib/")
                    break

        for locallib in lstLocalLibs:
            for llFile in lstLocalLibsFiles:
                if locallib + ".so" == llFile[:len(locallib) + 3]:
                    print("Copying /usr/local/lib/" + llFile + " to " + opts.chroot_dir + "/usr/local/lib/" + llFile)
                    cp_result = os.system("cp -p /usr/local/lib/" + llFile + " " + opts.chroot_dir + "/usr/local/lib/")
                    break

        # This is an additional loop required for mysqldb since there is an additional directory of mysql .so files
        # that need to be copied into chroot
        if opts.mysql:
            for myLib in lstMySqlLocalLibs:
                for myFile in lstMySqlLocalLibsFiles:
                    if myLib + ".so" == myFile[:len(myLib) + 3]:
                        print("Copying /usr/local/lib/mysql/" + myFile + " to " + opts.chroot_dir + "/usr/local/lib/mysql/" + myFile)
                        cp_result = os.system("cp -p /usr/local/lib/mysql/" + myFile + " " + opts.chroot_dir + "/usr/local/lib/mysql/")
                        break
        
        # Copies all of the python packages to the chroot directory so that they are available in that setting
        print("Copying Python packages")
        cp_result = os.system("cp -Rp /usr/local/lib/python" + python_version + " " + opts.chroot_dir + "/usr/local/lib/")
        
        # Runs the command to generate a new ld.hints.so file in the chroot directory.  This is required for Python
        # to run correctly in chroot
        print("Generating chrooted ld.hints.so file")
        ldconfig_chroot = "chroot %s /sbin/ldconfig /usr/local/lib" % opts.chroot_dir
        cp_result = os.system(ldconfig_chroot)
        
        # Sets the umask value back to its original setting
        new_umask = os.umask(old_umask)
